%include "boot.inc"

; 0柱面0磁道2扇區
[ORG  LOADER_BASE_ADDR]

[SECTION .data]
DESC_G_4K equ   1_000_0000_0000_0000_0000_0000b   ; G位 段界限的单位大小 1表示4K 0表示1字节
DESC_D_32 equ    1_00_0000_0000_0000_0000_0000b   ; 代码段 D位，0表示指令中的有效地址和操作数是16位，指令有效地址用IP寄存器。
                                                  ; 1表示指令中的有效地址及操作数是32位，指令有效地址用EIP寄存器。
DESC_B_32 equ    1_00_0000_0000_0000_0000_0000b   ; 栈段 B位 0表示使用的是sp寄存器，也就是栈的起始地址是16位寄存器的最大寻址范围，0xFFFF。
                                                  ; 1表示使用的是esp寄存器，也就是栈的起始地址是32位寄存器的最大寻址范围，0xFFFFFFFF。
DESC_L    equ     0_0_0000_0000_0000_0000_0000b   ; L位 0表示32位代码段 1表示64位代码段
DESC_AVL  equ       0_0000_0000_0000_0000_0000b   ; 操作系统用的，CPU不使用此位，暂置为0
DESC_LIMIT2 equ       1111_0000_0000_0000_0000b   ; 段界限
DESC_LIMIT2_VIDEO equ 0000_0000_0000_0000_0000b   ; 段界限
DESC_P    equ             1_000_0000_0000_0000b   ; P位 段是否存在 1存在 0不存在
DESC_DPL0 equ              00_0_0000_0000_0000b   ; DPL 特权级 数字越小 级别越高 0特权级
DESC_DPL1 equ              01_0_0000_0000_0000b   ; 1特权级
DESC_DPL2 equ              10_0_0000_0000_0000b   ; 2特权级
DESC_DPL3 equ              11_0_0000_0000_0000b   ; 3特权级
DESC_S_CODE equ               1_0000_0000_0000b   ; S位 非系统段
DESC_S_DATA equ      DESC_S_CODE
DESC_S_SYS  equ               0_0000_0000_0000b   ; 系统段
DESC_TYPE_CODE equ              1000_0000_0000b   ; x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.
DESC_TYPE_DATA equ              0010_0000_0000b   ; x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.

DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT2 + DESC_P + DESC_DPL0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00
DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT2 + DESC_P + DESC_DPL0 + DESC_S_CODE + DESC_TYPE_DATA + 0x00
DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT2_VIDEO + DESC_P + DESC_DPL0 + DESC_S_CODE + DESC_TYPE_DATA + 0x0b

RPL0     equ    00b     ; 0特权及
RPL1     equ    01b     ; 1特权及
RPL2     equ    10b     ; 2特权及
RPL3     equ    11b     ; 3特权及
TI_GDT	 equ   000b     ; TI位=0，表示使用GDT表
TI_LDT	 equ   100b     ; TI位=1，表示使用LDT表

PAGE_DIR_TABLE_POS equ 0x100000                   ; 低1M内存之后第一个字节

[SECTION .gdt]
gdt_base:
    dd 0, 0

gdt_code:
    dd 0x0000FFFF
    dd DESC_CODE_HIGH4

gdt_data:
    dd 0x0000FFFF
    dd DESC_DATA_HIGH4

gdt_video:
    dd 0x80000007	            ; limit=(0xbffff-0xb8000)/4k = 0x7fff/0x1000 = 0x7
                                ; 0xb8000的b在上边，所以这部分的段基址为0x8000
    dd DESC_VIDEO_HIGH4

gdt_ptr:
    dw $ - gdt_base -1
    dd gdt_base

ards_buf: times 20 db 0
pyhsical_memory: times 16 dd 0

CODE_SELECTOR  equ (1 << 3) + TI_GDT + RPL0     ; 选择子 选择子的高13位代表gdt表中段描述符文索引 1左移3位就代表gdt表中索引为1的段描述符文（TI位为0的情况下）
DATA_SELECTOR  equ (2 << 3) + TI_GDT + RPL0     ; 索引为2的段描述符
VIDEO_SELECTOR equ (3 << 3) + TI_GDT + RPL0     ; 索引为3的段描述符

[SECTION .text]
[BITS 16]
global _start
_start:
    mov     ax, 0
    mov     ss, ax
    mov     ds, ax
    mov     es, ax
    mov     fs, ax
    mov     gs, ax
    mov     si, ax

    mov     si, msg
    call    print

    call loader_physical_memory

    call enter_protected_mode

    jmp CODE_SELECTOR:protected_mode      ; 第一个段描述符的段基址为0, protected_mode就是真是的物理内存中的偏移地址

; --------------------------------------------------------
; 基于BIOS中断0x15 子功能0xE820 实现读取物理内存
; 存于 pyhsical_memory
loader_physical_memory:
    xor ebx, ebx
    mov di, ards_buf
    mov edx, 0x534D4150                   ; 'SMAP'
.invoke:
    mov eax, 0xE820
    mov ecx, 20

    int 0x15
    jc .error_hlt

    mov eax, [di]                         ; base_addr_low
    add eax, [di+8]                       ; length_low

    cmp esi, eax                          ; 和上次结果比较，大的放入esi寄存器中
    ja .next

    mov esi, eax
.next:
    cmp ebx, 0
    je .end
    loop .invoke
.error_hlt:
    hlt
.end:
    mov dword [pyhsical_memory], esi
    ret

enter_protected_mode:
    cli                                   ; 关中断
    lgdt [gdt_ptr]                        ; 加载gdt表

    in al, 0x92                           ; 打开A20总线
    or al, 0000_0010b
    out 0x92, al

    mov eax, cr0                          ; 开启保护模式
    or eax, 1
    mov cr0, eax
    ret

; --------------------------------------------------------
; 打印字符串
; si 传入字符串起始地址
print:
    mov ah, 0x0e
    mov bh, 0
    mov bl, 0x01
.loop:
    mov al, [si]
    cmp al, 0
    jz .done
    int 0x10

    inc si
    jmp .loop
.done:
    ret

msg:
    db "loader code is running...", 10, 13, 0

[BITS 32]
protected_mode:
    mov ax, DATA_SELECTOR
    mov ds, ax                                   ; 数据段选择子才能放在ds寄存器里面，代码段不行，CPU会校验
    mov ss, ax
    mov es, ax
    mov fs, ax
    mov gs, ax

    mov esp, LOADER_BASE_ADDR                    ; 重新赋值栈顶指针，不然下面在加载内核时会把栈空间覆盖掉，内核加载地址为0x9000时
                                               ; LOADER_BASE_ADDR=0xD00，刚好前面还有2K个字节可以使用

    mov eax, 5                                  ; 起始扇区号，我用的硬盘寻址是LBA，扇区编号从0开始，所以第6快扇区编号为5
    mov ebx, KERNEL_BASE_ADDR
    mov cx, 60
    call read_disk_lba_32

    call create_page_tab

    sgdt [gdt_ptr]

    mov ebx, [gdt_ptr + 2]
    or dword [ebx + 24 + 4], 0xc0000000         ; 给视频段的段基址变成虚拟的
    add dword [gdt_ptr + 2], 0xc0000000         ; 给gdt的基址也加上0xc0000000，变成内核所在的高地址

    mov eax, PAGE_DIR_TABLE_POS
    mov cr3, eax                                ; 页目录表的地址存入cr3寄存器

    mov eax, cr0
    or eax, 0x80000000
    mov cr0, eax                                ; 开启分页

    lgdt [gdt_ptr]

    mov ax, VIDEO_SELECTOR                      ; 使用第一个页目录项打印YOS
    mov es, ax
    mov byte [es:160*13+30*2], 'Y'
    mov byte [es:160*13+30*2+2], 'O'
    mov byte [es:160*13+30*2+4], 'S'

    mov eax, VIDEO_SELECTOR + 0xc0000000        ; 使用第768个页目录项打印YOS
    mov es, eax
    mov byte [es:160*14+30*2], 'Y'
    mov byte [es:160*14+30*2+2], 'O'
    mov byte [es:160*14+30*2+4], 'S'

    mov ax, DATA_SELECTOR
    mov es, ax
    jmp enter_kernel
enter_kernel:
    xor eax, eax
    xor ebx, ebx		                        ; ebx记录程序头表地址
    xor ecx, ecx		                        ; cx记录程序头表中的program header数量
    xor edx, edx		                        ; dx 记录program header尺寸,即e_phentsize

    mov dx, [KERNEL_BASE_ADDR + 42]	            ; e_phentsize：程序头表中每个条目的大小
    mov ebx, [KERNEL_BASE_ADDR + 28]            ; e_phoff：程序头表在文件中的偏移量
    add ebx, KERNEL_BASE_ADDR
    mov cx, [KERNEL_BASE_ADDR + 44]             ; e_phnum：程序头表中条目的数量

.loop_segment:
    mov ebp, ecx
    cmp byte [ebx + 0], 0		                ; 若p_type等于0,说明此program header未使用。
    je .PTNULL

    cld                                         ; df位置0，递增拷贝
    mov dword ecx, [ebx + 16]		            ; p_filesz：本段的大小
    mov eax, [ebx + 4]			                ; p_offset：本段在文件内的起始偏移字节
    add eax, KERNEL_BASE_ADDR	                ; eax为该段的物理地址
    mov esi, eax				                ; 源地址
    mov dword edi, [ebx + 8]			        ; 目的地址 p_vaddr
    rep movsb		                            ; 逐字节拷贝
.PTNULL:
    add ebx, edx				                ; edx为program header大小,即e_phentsize,在此ebx指向下一个program header
    mov ecx, ebp
    loop .loop_segment

    xchg bx, bx
    jmp KERNEL_ENTRY_POINT

; --------------------------------------------------------
; 创建页目录表、页表
create_page_tab:
    mov ecx, 1024
    mov ebx, 0
.clear:
    mov dword [PAGE_DIR_TABLE_POS + ebx*4], 0
    inc ebx
    loop .clear                                 ; 页目录表占4K空间，将前4K空间清0

.create_pde:
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x1000                             ; 第一个页目录项对应的第一个页表的位置
    mov ebx, eax                                ; 把这个先缓存下来，下边要用

    or eax, 111b                                ; RW=1,p=1,US=1,表示用户属性,所有特权级别都可以访问
    mov [PAGE_DIR_TABLE_POS], eax               ; 第一个页目录项的值
    mov [PAGE_DIR_TABLE_POS + 0xc00], eax       ; 第768个页目录项的值，高3-4G中第一个页目录项也对应到第一个页表
                                                ; 1024 × 3/4 = 768，768×4 = 0xc00
    sub eax, 0x1000
    mov [PAGE_DIR_TABLE_POS + 4092], eax        ; 将最后一个页表项指向页目录表的起始位置
                                                ; 注意是4092,而不是4096

    mov ecx, 256                                ; 映射1M的低端内存，每页4K，1M/4K = 256
    mov esi, 0
    mov edx, 111b                               ; 页表项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问
.create_pte:
    mov [ebx + esi*4], edx
    inc esi
    add edx, 0x1000                             ; 每页大小4K，每次加0x1000
    loop .create_pte

    ret

; --------------------------------------------------------
; 读硬盘
; eax 起始扇区号
; bx  将数据写入的内存地址
; cx  要读取的扇区数量，实际只用到cl
read_disk_lba_32:
    mov esi, eax                          ; 备份eax、cx
    mov di, cx

    mov dx, 0x1F2                         ; 0x1F2 8bit 指定读取或写入的扇区数
    mov al, cl
    out dx, al

    mov eax, esi
    mov dx, 0x1F3                         ; 0x1F3 8bit iba地址的前八位 0-7
    out dx, al

    mov cl, 8
    shr eax, cl
    mov dx, 0x1F4                         ; 0x1F4 8bit iba地址的中八位 8-15
    out dx, al

    shr eax, cl
    mov dx, 0x1F5                         ; 0x1F5 8bit iba地址的高八位 16-23
    out dx, al

    shr eax, cl
    mov dx, 0x1F6                         ; 0x1F6 低4位 0-3位iba地址的24-27，
    and al, 0b0000_1111                   ; 所以先将高4位置0，低4位保持不变
    or al, 0b1110_0000                    ; 再将高4位置为1110，
                                          ; 4 0表示主盘 1表示从盘，bochsrc中配置ata0-master代表Primary通道的主盘
                                          ; 5、7位固定为1
                                          ; 6 0表示CHS模式，1表示LAB模式，这里用的LBA模式
                                          ; 所以高4位为1110
    out dx, al

    mov dx, 0x1F7                         ; 0x1F7 8bit 命令或状态端口
    mov al, 0x20                          ; 0x20 读命令，0x30 写命令
    out dx, al

    mov cx, di                            ; di保存了要读取的扇区数
.loop:
    mov si, cx                            ; 双重循环，暂存cx
.no_ready:
    mov dx, 0x1F7                         ; 同一端口，还是0x1F7
    in al, dx
    and al, 0b10001000                    ; 只看第3位和第7位，
                                          ; 第3位为1表示硬盘控制器已准备好数据传输 0表示未准备好
                                          ; 第7位为1表示硬盘忙 0表示硬盘不忙
    cmp al, 0b00001000                    ; and之后期望为 0b0001000
    jnz .no_ready

    mov cx, 256                           ; 一个扇区512个字节，每次读一个字，所以循环256次
    mov dx, 0x1F0
.read_data:
    in ax, dx
    mov [ebx], ax
    add ebx, 2
    loop .read_data

    mov cx, si                            ; 恢复cx
    loop .loop
    ret
